前幾天把 Application Server 從一台變成多台,也把 Session 從 Server Memory 搬到共用的 Session Store:
瀏覽器 -> Load Balancer -> Server(多台,Stateless)-> Database
你可注意到了:Server 變多了,Database 卻還是只有一台。
不管前面有幾台 Server 在分攤流量,最後所有的讀寫都會匯聚到同一個 Database。
Application Layer 雖然順利做到了 Horizontal Scaling,Database 反而變成了新的瓶頸。
這也是大型系統很常出現的現象:解決了一個瓶頸,下一層往往會冒出新的瓶頸。
對一個社群平台來說,大部分操作其實都是 Read:
Write 則相對少很多:
多數使用者「看」的頻率遠高於「發」,所以 Read Traffic 通常遠高於 Write Traffic。
這帶出一個很自然的想法:
如果多準備幾個 Database,專門用來處理 Read,會不會就能分攤壓力?
這就是 Database Replication 要解決的問題。
Replication 的概念很直白:建立 Database 的複本(Replica)。
原本一個 Database 要同時處理讀跟寫,現在改成:
寫入固定走 Primary:
POST /posts
↓
Primary
讀取則交給 Replica:
GET /feed
↓
Replica
畫成架構圖:

原本所有 Request 都擠在同一個 Database,現在讓多個擁有相同資料的 Database Instance 一起分攤 Read Traffic。
有一點要特別澄清:這樣做還稱不上 Database 的 Horizontal Scaling。
因為每個 Replica 保存的都是同一份完整資料,真正把資料切開、分散存放到不同節點的做法叫 Sharding / Partitioning,這是之後才會碰到的主題(Day 18)。
Replication 目前解決的,單純是 Read Scaling。
不過,這個架構把讀跟寫拆到不同節點之後,也同時留下兩個要面對的代價:一個出在 Read 這一側,一個出在 Write 這一側,先從比較容易感覺到的 Read 開始看。
假設 Pikachu 剛發布一篇貼文,Request 成功寫進 Primary,但這筆資料同步到其他 Replica 需要一點時間。
如果 Pikachu 發文後立刻重整頁面,而這次 Request 剛好被導到還沒同步完成的 Replica,他可能會看不到自己剛剛發的文章。
這種「Primary 與 Replica 之間存在同步延遲」的現象,就是 Replication Lag。
換句話說,Replication 讓我們拿到了 Read Scalability,代價是要面對資料一致性(Consistency) 的問題。
這其實已經涉及分散式系統一個很經典的理論:CAP Theorem,不過這個主題份量不小,值得完整用一篇來討論,所以先留到日後再深入拆解。
Replication Lag 只是「讀到舊資料」的問題,頂多體驗不佳;Write 這一側藏著一個嚴重得多的問題
如果 Primary 本身掛掉了呢?
在 Primary-Replica 架構下,Primary 是整個系統唯一能寫入的節點。一旦 Primary 掛掉,即使旁邊還有好幾個 Replica 好端端地活著,整個系統照樣寫不進去任何資料,所以沒有人能發文、按讚、註冊新帳號。
Replica 掛掉的影響則小很多:Load Balancer 只要把這個掛掉的 Replica 從輪詢名單裡拿掉,Read 流量分給其他還活著的 Replica 就好,使用者頂多感覺載入慢一點,不會整個服務打不開。
正因為 Primary 是單點故障(Single Point of Failure),需要一套明確的處理流程,把某一個 Replica「升格」成新的 Primary,讓系統能繼續接受寫入。這個流程就叫 Failover。
畫成時間軸:
[偵測] Primary 沒回應 → 連續 N 次心跳失敗,判定為 Down
↓
[選舉] 從 Replica A / B / C 裡,挑資料最新的 Replica B
↓
[升級] Replica B 升級為新 Primary
↓
[改道] 所有 Write 流量改指向新 Primary(Replica B)
↓
[跟隨] Replica A、C 改成跟著新 Primary 同步
實務上,很少有團隊會自己從零刻一套 Failover 機制,常見做法是直接仰賴雲端服務商內建的能力(例如 AWS RDS 的 Multi-AZ 自動 Failover),或是採用成熟的開源工具(例如 PostgreSQL 生態的 Patroni、MySQL 生態的 Orchestrator)來處理偵測、選舉與流量切換這整套流程。
看完這套「偵測 → 選舉 → 切換」的流程,會發現 Primary-Replica 架構下,Write 這一側從故障到恢復,中間終究有一段無法寫入的空窗期。如果系統真的無法接受這段空窗,還有沒有別的架構選擇?
到目前為止採用的做法,正式名稱叫 Primary-Replica Replication:只有一個節點(Primary)能寫,其他節點(Replica)只能讀。
但這不是唯一的模式,另一種常見做法是 Multi-Primary Replication(也稱 Multi-Leader Replication):讓兩個(或多個)節點都能接受寫入,並且互相同步彼此的資料變化。
Primary A ⇄ Primary B
(兩邊都能 Write,也互相同步對方)
問題也很直接:如果同一瞬間,有人對 Primary A 寫入 nickname = "黃色老鼠",又有人對 Primary B 寫入同一筆資料的 nickname = "皮神",兩邊都成功寫入之後,到底哪一個才是「正確」的結果?
這種 Write Conflict(寫入衝突) 是 Multi-Primary 最大的挑戰,常見的處理方式包括:
這些機制都不簡單,這也是為什麼 Multi-Primary 通常只在真正需要「多個地點都要能寫入」的場景才會採用,例如之後會討論到的跨地區部署,讓每個地區都有自己的 Primary,本地寫入不需要跨海等待。
對現在規模的 PokeThreads 來說,Primary-Replica 已經夠用,沒必要為了還不存在的問題,提前引入 Multi-Primary 的複雜度。
今天透過 Database Replication,把 Read Traffic 從單一 Database 分攤到多個 Replica 上:

同時也拆解了 Primary-Replica 架構要付出的兩個代價:Read 這一側的 Replication Lag,以及 Write 這一側 Primary 故障時的 Failover 空窗期;並且看到 Multi-Primary 如何用「兩邊都能寫」換掉 Failover 的等待時間,但也換來了寫入衝突要處理。
Read 的壓力暫時緩解了,但每次 Feed Query 其實都要重新查一次 Database(不管是 Primary 還是 Replica),如果同樣的資料一直被大量使用者重複讀取,這樣做真的有必要嗎?這是明天要討論的問題。